// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "ios/chrome/browser/ui/settings/search_engine_table_view_controller.h"

#import <memory>

#import "base/apple/foundation_util.h"
#import "base/command_line.h"
#import "base/files/scoped_temp_dir.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "components/country_codes/country_codes.h"
#import "components/password_manager/core/common/password_manager_features.h"
#import "components/policy/core/common/mock_policy_service.h"
#import "components/policy/core/common/policy_namespace.h"
#import "components/policy/core/common/schema_registry.h"
#import "components/policy/policy_constants.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/search_engines/search_engines_pref_names.h"
#import "components/search_engines/search_engines_switches.h"
#import "components/search_engines/template_url_data_util.h"
#import "components/search_engines/template_url_prepopulate_data.h"
#import "components/search_engines/template_url_service.h"
#import "components/strings/grit/components_strings.h"
#import "components/sync_preferences/testing_pref_service_syncable.h"
#import "ios/chrome/browser/favicon/favicon_service_factory.h"
#import "ios/chrome/browser/favicon/ios_chrome_favicon_loader_factory.h"
#import "ios/chrome/browser/favicon/ios_chrome_large_icon_service_factory.h"
#import "ios/chrome/browser/history/model/history_service_factory.h"
#import "ios/chrome/browser/policy/model/browser_state_policy_connector_mock.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/model/browser_state/test_chrome_browser_state.h"
#import "ios/chrome/browser/shared/ui/table_view/legacy_chrome_table_view_controller_test.h"
#import "ios/chrome/browser/ui/settings/cells/legacy_settings_search_engine_item.h"
#import "ios/web/public/test/web_task_environment.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "ui/base/l10n/l10n_util_mac.h"

using TemplateURLPrepopulateData::GetAllPrepopulatedEngines;
using TemplateURLPrepopulateData::PrepopulatedEngine;

namespace {

typedef struct {
  const std::string short_name;
  const GURL searchable_url;
} SearchEngine;

// Unit tests for SearchEngineTableViewController when
// `kSearchEngineChoiceTrigger` is enabled.
class SearchEngineTableViewControllerChoiceScreenTest
    : public LegacyChromeTableViewControllerTest {
 protected:
  void SetUp() override {
    LegacyChromeTableViewControllerTest::SetUp();

    // Create prepopulated search engine.
    prepopulated_search_engine_.push_back(
        {"google.com", GURL("https://google.com?q={searchTerms}")});
    // Create custom search engine.
    custom_search_engine_.push_back(
        {"custom-1", GURL("https://c1.com?q={searchTerms}")});
    custom_search_engine_.push_back(
        {"custom-2", GURL("https://c2.com?q={searchTerms}")});
    custom_search_engine_.push_back(
        {"custom-3", GURL("https://c3.com?q={searchTerms}")});
    custom_search_engine_.push_back(
        {"custom-4", GURL("https://c4.com?q={searchTerms}")});

    TestChromeBrowserState::Builder test_cbs_builder;

    test_cbs_builder.AddTestingFactory(
        ios::TemplateURLServiceFactory::GetInstance(),
        ios::TemplateURLServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        ios::FaviconServiceFactory::GetInstance(),
        ios::FaviconServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        IOSChromeLargeIconServiceFactory::GetInstance(),
        IOSChromeLargeIconServiceFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        IOSChromeFaviconLoaderFactory::GetInstance(),
        IOSChromeFaviconLoaderFactory::GetDefaultFactory());
    test_cbs_builder.AddTestingFactory(
        ios::HistoryServiceFactory::GetInstance(),
        ios::HistoryServiceFactory::GetDefaultFactory());
    chrome_browser_state_ = test_cbs_builder.Build();
    // Override the country checks to simulate being in Belgium.
    pref_service_ = chrome_browser_state_->GetTestingPrefService();
    base::CommandLine::ForCurrentProcess()->AppendSwitchASCII(
        switches::kSearchEngineChoiceCountry, "BE");
    DefaultSearchManager::SetFallbackSearchEnginesDisabledForTesting(true);
    template_url_service_ = ios::TemplateURLServiceFactory::GetForBrowserState(
        chrome_browser_state_.get());
    template_url_service_->Load();
  }

  void TearDown() override {
    DefaultSearchManager::SetFallbackSearchEnginesDisabledForTesting(false);
    [base::apple::ObjCCastStrict<SearchEngineTableViewController>(controller())
        settingsWillBeDismissed];
    LegacyChromeTableViewControllerTest::TearDown();
  }

  LegacyChromeTableViewController* InstantiateController() override {
    return [[SearchEngineTableViewController alloc]
        initWithBrowserState:chrome_browser_state_.get()];
  }

  // Adds a prepopulated search engine to TemplateURLService.
  // `prepopulate_id` should be big enough (>1000) to avoid collision with real
  // prepopulated search engines. The collision happens when
  // TemplateURLService::SetUserSelectedDefaultSearchProvider is called, in the
  // callback of PrefService the DefaultSearchManager will update the searchable
  // URL of default search engine from prepopulated search engines list.
  TemplateURL* AddPriorSearchEngine(const SearchEngine& search_engine,
                                    int prepopulate_id,
                                    bool set_default) {
    TemplateURLData data;
    data.SetShortName(base::ASCIIToUTF16(search_engine.short_name));
    data.SetKeyword(base::ASCIIToUTF16(search_engine.short_name));
    data.SetURL(search_engine.searchable_url.possibly_invalid_spec());
    data.favicon_url =
        TemplateURL::GenerateFaviconURL(search_engine.searchable_url);
    data.prepopulate_id = prepopulate_id;
    TemplateURL* url =
        template_url_service_->Add(std::make_unique<TemplateURL>(data));
    if (set_default) {
      template_url_service_->SetUserSelectedDefaultSearchProvider(url);
    }
    return url;
  }

  // Adds a custom search engine to TemplateURLService.
  TemplateURL* AddCustomSearchEngine(const SearchEngine& search_engine,
                                     base::Time last_visited_time,
                                     bool set_default) {
    TemplateURLData data;
    data.SetShortName(base::ASCIIToUTF16(search_engine.short_name));
    data.SetKeyword(base::ASCIIToUTF16(search_engine.short_name));
    data.SetURL(search_engine.searchable_url.possibly_invalid_spec());
    data.favicon_url =
        TemplateURL::GenerateFaviconURL(search_engine.searchable_url);
    data.last_visited = last_visited_time;
    TemplateURL* url =
        template_url_service_->Add(std::make_unique<TemplateURL>(data));
    if (set_default) {
      template_url_service_->SetUserSelectedDefaultSearchProvider(url);
    }
    return url;
  }

  void CheckItem(NSString* expected_text,
                 NSString* expected_detail_text,
                 const GURL& expected_url,
                 bool expected_checked,
                 int section,
                 int row,
                 bool enabled) {
    SettingsSearchEngineItem* item =
        base::apple::ObjCCastStrict<SettingsSearchEngineItem>(
            GetTableViewItem(section, row));
    EXPECT_NSEQ(expected_text, item.text);
    EXPECT_NSEQ(expected_detail_text, item.detailText);
    EXPECT_EQ(expected_checked ? UITableViewCellAccessoryCheckmark
                               : UITableViewCellAccessoryNone,
              item.accessoryType);
    EXPECT_EQ(enabled, item.enabled);
  }

  // Checks a SettingsSearchEngineItem with data from a fabricated
  // TemplateURL. The SettingsSearchEngineItem in the `row` of `section`
  // should contain a title and a subtitle that are equal to `expected_text` and
  // an URL which can be generated by filling empty query word into
  // `expected_searchable_url`. If `expected_checked` is true, the
  // SettingsSearchEngineItem should have a
  // UITableViewCellAccessoryCheckmark.
  void CheckPrepopulatedItem(const SearchEngine& expected_search_engine,
                             bool expected_checked,
                             int section,
                             int row,
                             bool enabled = true) {
    TemplateURLData data;
    data.SetURL(expected_search_engine.searchable_url.possibly_invalid_spec());
    const std::string expected_url =
        TemplateURL(data).url_ref().ReplaceSearchTerms(
            TemplateURLRef::SearchTermsArgs(std::u16string()),
            template_url_service_->search_terms_data());
    CheckItem(base::SysUTF8ToNSString(expected_search_engine.short_name),
              base::SysUTF8ToNSString(expected_search_engine.short_name),
              expected_search_engine.searchable_url, expected_checked, section,
              row, enabled);
  }

  // Checks a SettingsSearchEngineItem with data from a fabricated
  // TemplateURL. The SettingsSearchEngineItem in the `row` of `section`
  // should contain a title and a subtitle that are equal to `expected_text` and
  // an URL which can be generated from `expected_searchable_url` by
  // TemplateURL::GenerateFaviconURL. If `expected_checked` is true, the
  // SettingsSearchEngineItem should have a
  // UITableViewCellAccessoryCheckmark.
  void CheckCustomItem(const SearchEngine& expected_search_engine,
                       bool expected_checked,
                       int section,
                       int row,
                       bool enabled = true) {
    CheckItem(
        base::SysUTF8ToNSString(expected_search_engine.short_name),
        base::SysUTF8ToNSString(expected_search_engine.short_name),
        TemplateURL::GenerateFaviconURL(expected_search_engine.searchable_url),
        expected_checked, section, row, enabled);
  }

  // Checks a SettingsSearchEngineItem with data from a real prepopulated
  // TemplateURL. The SettingsSearchEngineItem in the `row` of `section`
  // should contain a title equal to `expected_text`, a subtitle equal to
  // `expected_detail_text`, and an URL equal to `expected_favicon_url`. If
  // `expected_checked` is true, the SettingsSearchEngineItem should have
  // a UITableViewCellAccessoryCheckmark.
  void CheckRealItem(const TemplateURL* turl,
                     bool expected_checked,
                     int section,
                     int row,
                     bool enabled = true) {
    CheckItem(base::SysUTF16ToNSString(turl->short_name()),
              base::SysUTF16ToNSString(turl->keyword()),
              GURL(turl->url_ref().ReplaceSearchTerms(
                  TemplateURLRef::SearchTermsArgs(std::u16string()),
                  template_url_service_->search_terms_data())),
              expected_checked, section, row, enabled);
  }

  // Deletes items at `indexes` and wait util condition returns true or timeout.
  [[nodiscard]] bool DeleteItemsAndWait(NSArray<NSIndexPath*>* indexes,
                                        ConditionBlock condition) {
    SearchEngineTableViewController* searchEngineController =
        static_cast<SearchEngineTableViewController*>(controller());
    [searchEngineController deleteItems:indexes];
    return base::test::ios::WaitUntilConditionOrTimeout(
        base::test::ios::kWaitForUIElementTimeout, condition);
  }

  web::WebTaskEnvironment task_environment_;
  base::test::ScopedFeatureList feature_list_{
      switches::kSearchEngineChoiceTrigger};
  std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
  base::HistogramTester histogram_tester_;
  TemplateURLService* template_url_service_;  // weak
  sync_preferences::TestingPrefServiceSyncable* pref_service_;
  std::vector<const SearchEngine> prepopulated_search_engine_;
  std::vector<const SearchEngine> custom_search_engine_;
};

// Tests that the table's first section subtitle is correctly set.
TEST_F(SearchEngineTableViewControllerChoiceScreenTest, TestSectionSubtitle) {
  AddPriorSearchEngine(prepopulated_search_engine_[0], 1001, true);

  CreateController();
  CheckController();

  TableViewHeaderFooterItem* header =
      [[controller() tableViewModel] headerForSectionIndex:0];
  ASSERT_TRUE([header respondsToSelector:@selector(subtitle)]);
  EXPECT_NSEQ(
      l10n_util::GetNSString(IDS_SEARCH_ENGINE_CHOICE_SETTINGS_SUBTITLE),
      [(id)header subtitle]);
}

// Tests that items are displayed correctly when a prepopulated search engine is
// selected as default.
TEST_F(SearchEngineTableViewControllerChoiceScreenTest,
       TestChoiceScreenUrlsLoadedWithPrepopulatedSearchEngineAsDefault) {
  AddCustomSearchEngine(custom_search_engine_[0],
                        base::Time::Now() - base::Seconds(10), false);
  AddCustomSearchEngine(custom_search_engine_[1],
                        base::Time::Now() - base::Hours(10), false);

  // GetTemplateURLForChoiceScreen checks for the default search engine. Since
  // default fallback is disabled for testing, we set a fake prepopulated one
  // then change it.
  AddPriorSearchEngine(prepopulated_search_engine_[0], 1001, true);
  std::vector<std::unique_ptr<TemplateURL>> prepopulated_engines =
      template_url_service_->GetTemplateURLsForChoiceScreen();
  // Set a real prepopulated engine as the default.
  TemplateURL* default_search_engine = prepopulated_engines[0].get();
  template_url_service_->SetUserSelectedDefaultSearchProvider(
      default_search_engine);

  CreateController();
  CheckController();

  ASSERT_EQ(2, NumberOfSections());
  // There are twelve required prepopulated search engines plus the previously
  // added default one.
  ASSERT_EQ(13, NumberOfItemsInSection(0));
  CheckRealItem(default_search_engine, true, 0, 0);
  for (size_t i = 1; i < prepopulated_engines.size(); i++) {
    CheckRealItem(prepopulated_engines[i].get(), false, 0, i);
  }
  ASSERT_EQ(2, NumberOfItemsInSection(1));
  CheckCustomItem(custom_search_engine_[0], false, 1, 0);
  CheckCustomItem(custom_search_engine_[1], false, 1, 1);

  // Select another default engine by user interaction.
  [controller() tableView:controller().tableView
      didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:1 inSection:0]];
  SInt64 written_pref = pref_service_->GetInt64(
      prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp);
  // We don't care about the specific value, we just need to check that
  // something was written.
  ASSERT_FALSE(written_pref == 0);
  // Select another default engine by user interaction.
  [controller() tableView:controller().tableView
      didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:2 inSection:0]];
  // This time make sure that the pref was not re-written.
  ASSERT_EQ(written_pref,
            pref_service_->GetInt64(
                prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp));
}

// Tests that items are displayed correctly when a custom search engine is
// selected as default.
TEST_F(SearchEngineTableViewControllerChoiceScreenTest,
       TestChoiceScreenUrlsLoadedWithCustomSearchEngineAsDefault) {
  AddCustomSearchEngine(custom_search_engine_[0],
                        base::Time::Now() - base::Seconds(10), true);
  AddCustomSearchEngine(custom_search_engine_[1],
                        base::Time::Now() - base::Hours(10), false);

  std::vector<std::unique_ptr<TemplateURL>> prepopulated_engines =
      template_url_service_->GetTemplateURLsForChoiceScreen();

  CreateController();
  CheckController();

  ASSERT_EQ(2, NumberOfSections());
  // There are twelve required prepopulated search egines plus the default
  // custom one, which should be the first in the list.
  ASSERT_EQ(13, NumberOfItemsInSection(0));
  CheckCustomItem(custom_search_engine_[0], true, 0, 0);
  for (size_t i = 1; i < prepopulated_engines.size(); i++) {
    CheckRealItem(prepopulated_engines[i].get(), false, 0, i);
  }
  ASSERT_EQ(1, NumberOfItemsInSection(1));
  CheckCustomItem(custom_search_engine_[1], false, 1, 0);

  // Select another default engine by user interaction.
  [controller() tableView:controller().tableView
      didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]];
  // We don't care about the value, we just need to check that something was
  // written.
  ASSERT_FALSE(
      pref_service_->GetInt64(
          prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp) == 0);
}

// Tests that prepopulated engines are correctly displayed when enabling and
// disabling edit mode.
TEST_F(SearchEngineTableViewControllerChoiceScreenTest, EditingMode) {
  AddCustomSearchEngine(custom_search_engine_[1],
                        base::Time::Now() - base::Minutes(10), true);
  AddCustomSearchEngine(custom_search_engine_[0],
                        base::Time::Now() - base::Seconds(10), false);

  SearchEngineTableViewController* searchEngineController =
      static_cast<SearchEngineTableViewController*>(controller());
  EXPECT_TRUE([searchEngineController editButtonEnabled]);

  // Set the first prepopulated engine as default engine using
  // `template_url_service_`. This will reload the table and move C2 to the
  // second list.
  std::vector<std::unique_ptr<TemplateURL>> urls_for_choice_screen =
      template_url_service_->GetTemplateURLsForChoiceScreen();
  // The first engine in the list is C2.
  template_url_service_->SetUserSelectedDefaultSearchProvider(
      urls_for_choice_screen[1].get());
  CheckRealItem(urls_for_choice_screen[1].get(), true, 0, 0);

  // Enable editing mode
  [searchEngineController setEditing:YES animated:NO];
  // Prepopulated engines should be disabled with checkmark removed.
  for (size_t i = 1; i < urls_for_choice_screen.size(); i++) {
    CheckRealItem(urls_for_choice_screen[i].get(), false, 0, i - 1, false);
  }
  CheckCustomItem(custom_search_engine_[0], false, 1, 0);
  CheckCustomItem(custom_search_engine_[1], false, 1, 1);

  // Disable editing mode
  [searchEngineController setEditing:NO animated:NO];
  // Prepopulated engines should be re-enabled and the checkmark should be back.
  CheckRealItem(urls_for_choice_screen[1].get(), true, 0, 0);
  for (size_t i = 2; i < urls_for_choice_screen.size(); i++) {
    CheckRealItem(urls_for_choice_screen[i].get(), false, 0, i - 1);
  }
  CheckCustomItem(custom_search_engine_[0], false, 1, 0);
  CheckCustomItem(custom_search_engine_[1], false, 1, 1);
}

// Tests that custom search engines can be deleted, and if default engine is
// deleted it will be reset to the first prepopulated engine.
TEST_F(SearchEngineTableViewControllerChoiceScreenTest, DeleteItems) {
  AddCustomSearchEngine(custom_search_engine_[0],
                        base::Time::Now() - base::Seconds(10), false);
  AddCustomSearchEngine(custom_search_engine_[1],
                        base::Time::Now() - base::Minutes(10), false);
  AddCustomSearchEngine(custom_search_engine_[2],
                        base::Time::Now() - base::Hours(10), true);
  AddCustomSearchEngine(custom_search_engine_[3],
                        base::Time::Now() - base::Days(1), false);

  CreateController();
  CheckController();

  // This method returns the list of prepopulated items and the custom search
  // engine that was set as default.
  int number_of_prepopulated_items =
      static_cast<int>(
          template_url_service_->GetTemplateURLsForChoiceScreen().size()) -
      1;
  ASSERT_EQ(2, NumberOfSections());
  ASSERT_EQ(number_of_prepopulated_items + 1, NumberOfItemsInSection(0));
  ASSERT_EQ(3, NumberOfItemsInSection(1));

  // Remove C3 from first list and C1 from second list.
  ASSERT_TRUE(DeleteItemsAndWait(
      @[
        [NSIndexPath indexPathForRow:0 inSection:0],
        [NSIndexPath indexPathForRow:0 inSection:1]
      ],
      ^{
        return NumberOfItemsInSection(0) == number_of_prepopulated_items;
      }));
  ASSERT_TRUE(NumberOfItemsInSection(1) == 2);

  // Select C4 as default engine by user interaction.
  [controller() tableView:controller().tableView
      didSelectRowAtIndexPath:[NSIndexPath indexPathForRow:0 inSection:1]];
  // We don't care about the value, we just need to check that something was
  // written.
  ASSERT_FALSE(
      pref_service_->GetInt64(
          prefs::kDefaultSearchProviderChoiceScreenCompletionTimestamp) == 0);

  ASSERT_EQ(number_of_prepopulated_items + 1, NumberOfItemsInSection(0));
  ASSERT_EQ(2, NumberOfItemsInSection(1));

  // Remove all custom search engines.
  ASSERT_TRUE(DeleteItemsAndWait(
      @[
        [NSIndexPath indexPathForRow:0 inSection:0],
        [NSIndexPath indexPathForRow:0 inSection:1],
        [NSIndexPath indexPathForRow:1 inSection:1]
      ],
      ^{
        return NumberOfSections() == 1;
      }));
  ASSERT_TRUE(NumberOfItemsInSection(0) == number_of_prepopulated_items);
}

}  // namespace
